// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <memory>

#include <assert.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <threads.h>
#include <unistd.h>

#include <ddk/binding.h>
#include <ddk/debug.h>
#include <ddk/device.h>
#include <ddk/driver.h>
#include <ddk/platform-defs.h>
#include <ddk/protocol/scpi.h>
#include <hw/reg.h>

#include <soc/aml-s912/s912-hw.h>

#include <fbl/algorithm.h>
#include <fbl/unique_ptr.h>
#include <zircon/assert.h>
#include <zircon/process.h>
#include <zircon/syscalls.h>
#include <zircon/threads.h>

#include "vim.h"

namespace vim {

// TODO(rjascani): Remove this when not needed for testing any longer
const pbus_dev_t tee_dev = []() {
  pbus_dev_t dev = {};
  dev.name = "tee";
  dev.vid = PDEV_VID_GENERIC;
  dev.pid = PDEV_PID_GENERIC;
  dev.did = PDEV_DID_OPTEE;
  return dev;
}();

zx_status_t Vim::Create(void* ctx, zx_device_t* parent) {
  pbus_protocol_t pbus;
  iommu_protocol_t iommu;
  pbus_board_info_t info = {};

  auto status = device_get_protocol(parent, ZX_PROTOCOL_PBUS, &pbus);
  if (status != ZX_OK) {
    return status;
  }

  // Set dummy board revision to facilitate testing of platform device get_board_info support.
  info.board_revision = 1234;
  pbus_set_board_info(&pbus, &info);

  status = device_get_protocol(parent, ZX_PROTOCOL_IOMMU, &iommu);
  if (status != ZX_OK) {
    return status;
  }

  fbl::AllocChecker ac;
  auto board = std::unique_ptr<Vim>(new (&ac) Vim(parent, &pbus, &iommu));
  if (!ac.check()) {
    return ZX_ERR_NO_MEMORY;
  }

  status = board->DdkAdd("vim", DEVICE_ADD_NON_BINDABLE);
  if (status != ZX_OK) {
    return status;
  }

  // Start up our protocol helpers and platform devices.
  status = board->Start();

  if (status == ZX_OK) {
    // devmgr is now in charge of the device.
    __UNUSED auto* dummy = board.release();
  }

  return status;
}

int Vim::Thread() {
  zx_status_t status;
  pdev_board_info_t info;

  // Fetch the board info so that we can distinguish between the "vim2" and
  // "vim2-machina" boards. The latter of which does not initialize devices.
  status = pbus_.GetBoardInfo(&info);
  if (status != ZX_OK) {
    zxlogf(ERROR, "Thread: GetBoardInfo failed: %d\n", status);
    return -1;
  }

  if (info.pid == PDEV_PID_VIM2_MACHINA) {
    return ZX_OK;
  }

  // Start protocol drivers before adding platform devices.
  // Sysmem is started early so zx_vmo_create_contiguous() works.
  if ((status = SysmemInit()) != ZX_OK) {
    zxlogf(ERROR, "Thread: SysmemInit failed: %d\n", status);
    return -1;
  }

  if ((status = GpioInit()) != ZX_OK) {
    zxlogf(ERROR, "Thread: GpioInit failed: %d\n", status);
    return -1;
  }

  if ((status = I2cInit()) != ZX_OK) {
    zxlogf(ERROR, "Thread: I2cInit failed: %d\n", status);
    return -1;
  }

  if ((status = ClkInit()) != ZX_OK) {
    zxlogf(ERROR, "Thread: ClkInit failed: %d\n", status);
    return -1;
  }

  if ((status = CanvasInit()) != ZX_OK) {
    zxlogf(ERROR, "Thread: CanvasInit failed: %d\n", status);
    return -1;
  }

  // Start platform devices.
  if ((status = UartInit()) != ZX_OK) {
    zxlogf(ERROR, "Thread: UartInit failed: %d\n", status);
    return -1;
  }

  if ((status = EmmcInit()) != ZX_OK) {
    zxlogf(ERROR, "Thread: EmmcInit failed: %d\n", status);
    return -1;
  }

  if ((status = SdioInit()) != ZX_OK) {
    zxlogf(ERROR, "Thread: SdioInit failed: %d\n", status);
    return -1;
  }

  if ((status = EthInit()) != ZX_OK) {
    zxlogf(ERROR, "Thread: EthInit failed: %d\n", status);
    return -1;
  }

  if ((status = UsbInit()) != ZX_OK) {
    zxlogf(ERROR, "Thread: UsbInit failed: %d\n", status);
    return -1;
  }

  if ((status = MaliInit()) != ZX_OK) {
    zxlogf(ERROR, "Thread: MaliInit failed: %d\n", status);
    return -1;
  }

  if ((status = ThermalInit()) != ZX_OK) {
    zxlogf(ERROR, "Thread: ThermalInit failed: %d\n", status);
    return -1;
  }

  if ((status = DisplayInit()) != ZX_OK) {
    zxlogf(ERROR, "Thread: DisplayInit failed: %d\n", status);
    return -1;
  }

  if ((status = VideoInit()) != ZX_OK) {
    zxlogf(ERROR, "Thread: VideoInit failed: %d\n", status);
    return -1;
  }

  // Remove this when not needed for testing any longer
  if ((status = pbus_.DeviceAdd(&tee_dev)) != ZX_OK) {
    zxlogf(ERROR, "vim_start_thread, could not add tee_dev: %d\n", status);
    return -1;
  }

  if ((status = SdInit()) != ZX_OK) {
    zxlogf(ERROR, "Thread: SdInit failed: %d\n", status);
    return -1;
  }

  return ZX_OK;
}

zx_status_t Vim::Start() {
  int rc = thrd_create_with_name(
      &thread_, [](void* arg) -> int { return reinterpret_cast<Vim*>(arg)->Thread(); }, this,
      "vim-start-thread");

  if (rc != thrd_success) {
    return ZX_ERR_INTERNAL;
  }
  return ZX_OK;
}

void Vim::DdkRelease() { delete this; }
zx_driver_ops_t vim_driver_ops = []() {
  zx_driver_ops_t ops = {};
  ops.version = DRIVER_OPS_VERSION;
  ops.bind = Vim::Create;
  return ops;
}();

}  // namespace vim

ZIRCON_DRIVER_BEGIN(vim, vim::vim_driver_ops, "zircon", "0.1", 4)
BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_PBUS),
    BI_ABORT_IF(NE, BIND_PLATFORM_DEV_VID, PDEV_VID_KHADAS),
    BI_MATCH_IF(EQ, BIND_PLATFORM_DEV_PID, PDEV_PID_VIM2),
    BI_MATCH_IF(EQ, BIND_PLATFORM_DEV_PID, PDEV_PID_VIM2_MACHINA), ZIRCON_DRIVER_END(vim)
